/*
===========================================================================
Copyright (C) 2005 Sarunas

This file is part of MyPIRC source code.

DisksDB source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

DisksDB source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with DisksDB; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/
using System;
using System.Collections;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

namespace MyIRC
{
	/// <summary>
	/// Irc client class which represents connected client to server.
	/// Has ability to join channels, open private chats, etc.
	/// </summary>
	public class IrcClient : BaseObject, IConnection
	{
		public event EventHandlerRawMessage RawMessageRecieved;
		public event EventHandlerConnected Connected;
		public event EventHandlerPingPong PingPong;
		public event EventHandlerDisconnected Disconnected;
		public event EventHandlerUnparsedLeftText UnparsedLeftText;
		public event EventHandlerChannelJoined ChannelJoined;
		public event EventHandlerPrivateChatCreated PrivateChatCreated;
		public event EventHandlerNotice Notice;

		private ArrayList privates = new ArrayList();
		private ArrayList users = new ArrayList();
		private ArrayList channels = new ArrayList();
		private Thread thread = null;
		private TcpClient tcpClient = null;
		private NetworkStream networkStream = null;
		private ClientUser user = null;
		private string serverAddress = null;
		private	int serverPort = 6667;
		private bool createPrivatesForNotice = false;

		/// <summary>
		/// Constructs CServer class
		/// </summary>
		public IrcClient() : base(null)
		{
			this.user = new ClientUser(this);
		}

		/// <summary>
		/// Connects to specified server. If is connected then disconnects and
		/// connects to given server.
		/// </summary>
		/// <param name="name">server address ex.: irc.dalnet.lt</param>
		/// <param name="port">server port ex.: 6667</param>
		public void Connect(string name, int port)
		{
			Monitor.Enter(this);

			if (name == null)
			{
				this.serverAddress = "127.0.0.1";
			} 
			else if ("".Equals(name))
			{
				this.serverAddress = "127.0.0.1";
			} 
			else
			{
				this.serverAddress = name;
			}
			this.serverPort = port;

			Monitor.Exit(this);

			if (this.IsConnected)
			{
				this.Disconnect();
			}

			Monitor.Enter(this);

			this.thread = new Thread(new ThreadStart(this.WorkerThread));
			thread.Start();

			Monitor.Exit(this);
		}

		/// <summary>
		/// Disconnects from server.
		/// Then disconnected Disconnected event raised
		/// </summary>
		public void Disconnect()
		{
			Disconnect("");
		}

		/// <summary>
		/// Disconnects from server. Waits until clinet thread is stopped.
		/// </summary>
		/// <param name="message">Quit message</param>
		public void Disconnect(string message)
		{
			if (true == this.IsConnected)
			{
				this.SendMessage("QUIT :" + message);
				this.SetConnected(false);
			}

			OnDisconnected();
		}

		internal void Remove(Private p)
		{
			this.privates.Remove(p);
		}

		internal void Remove(Channel p)
		{
			this.channels.Remove(p);
		}

		public void SendMessage(string message)
		{
			this.CheckConnected();

			if (message.Length < 512)
			{
				try
				{
					if(networkStream.CanWrite)
					{
						Byte[] sendBytes = Encoding.ASCII.GetBytes(message + "\n");
						networkStream.Write(sendBytes, 0, sendBytes.Length);
					}
				}
				catch (Exception ex)
				{
					PrintException(ex);

					this.SetConnected(false);

					OnDisconnected();
				}		
			}
		}

		/// <summary>
		/// Gets user configuration
		/// </summary>
		public IUser User
		{
			get
			{
				return this.user;
			}
		}

		public void JoinChannel(string name, string key)
		{
			SendMessage("JOIN " + name + " " + key);
		}

		public void QueryNick(string nick)
		{
			if ( (null == nick) || ("".Equals(nick)))
			{
				return;
			}

			//IrcUser user = FindUser(nick, true);

			FindPrivate(nick, true);
		}

		private void AddText(string text)
		{
			if ( (null != text) && ("".Equals(text) == false) )
			{
				text = text.Trim('\n', '\r');

				OnRawMessageRecieved(text);

				if (false == ParseString(text))
				{
					System.Diagnostics.Debug.WriteLine("["+ text +"]");
				}
			}
		}

		private bool ParseString(string text)
		{
			Regex r = new Regex("[ ]");

			string[] words = r.Split(text);

			if (words.Length == 0) return false;

			if ("PING".Equals(words[0]))
			{
				char[] tmp = text.ToCharArray();
				tmp[1] = 'O';

				SendMessage(new string(tmp));
				
				OnPingPong();

				return true;
			}

			return ParseMessage(text, words);
		}

		/// <summary>
		/// Client worker thread function.
		/// </summary>
		private void WorkerThread()
		{
			string tmpMsg = "";
			byte[] buf = new byte[1024];

			try
			{
				this.tcpClient = new System.Net.Sockets.TcpClient();
				this.tcpClient.Connect(this.serverAddress, this.serverPort);
				this.networkStream = this.tcpClient.GetStream();

				Monitor.Enter(this);

				this.SetConnected(true);
				string nick = (string)this.user.Nick.Clone();
				string user = (string)this.user.Ident.Clone();
				string name = (string)this.user.RealName.Clone();
				string addr = (string)this.serverAddress.Clone();

				Monitor.Exit(this);
				
				SendMessage("NICK " + nick);
				SendMessage("USER " + user + " localhost " + addr + " :" + name);
			}
			catch (Exception ex)
			{
				PrintException(ex);

				Monitor.Enter(this);

				this.SetConnected(false);

				Monitor.Exit(this);
				
				try
				{
					this.networkStream.Close();
					this.tcpClient.Close();
				}
				catch (Exception ex1)
				{
					PrintException(ex1);
				}

				OnDisconnected();

				return;
			}

			OnConnected();

			while (IsConnected)
			{
				Thread.Sleep(100);

				try
				{
					if (this.networkStream.DataAvailable)
					{
						int size = this.networkStream.Read(buf, 0, 1024);
					
						for (int i = 0; i < size; i++)
						{
							if ( ((char)buf[i] == '\n') || ((char)buf[i] == 0xA) )
							{
								if (tmpMsg.Length > 0)
								{
									this.AddText(tmpMsg);
									tmpMsg = "";
								}
							} 
							else
							{
								tmpMsg += (char)buf[i];
							}
						}
					}
				}
				catch (Exception ex)
				{
					PrintException(ex);

					Monitor.Enter(this);

					SetConnected(false);

					Monitor.Exit(this);

					break;
				}
			}

			try
			{
				this.networkStream.Close();
				this.tcpClient.Close();
			}
			catch (Exception ex)
			{
				PrintException(ex);
			}

			OnDisconnected();
		}

		private bool ParsePrivMsg(string msg, string[] parameters)
		{
			if (parameters.Length <= 3)
			{
				return false;
			}

			string sender = parameters[0];
			string reciever = parameters[2];

			int pos1 = msg.IndexOf(" :", parameters[0].Length + parameters[1].Length + parameters[2].Length + 2);
			string message = (pos1 >= 0) ? msg.Substring(pos1 + 2) : string.Empty;

			if ((reciever[0] == '#') || (reciever[0] == '&'))
			{
				Channel chan = this.FindChannel(reciever, true);

				if (null != chan)
				{
					//int pos1 = msg.IndexOf(':', 1);
					//chan.OnMessageAdded(words[0], (pos1 >= 0) ? msg.Substring(pos1 + 1) : null);
					chan.OnMessageAdded(sender, message);
					return true;
				}
			} 
			else
			{
				Private p = FindPrivate(sender, true);

				if (null != p)
				{
					//int pos1 = msg.IndexOf(':', 1);
					//p.OnPrivateMessage(words[0], (pos1 >= 0) ? msg.Substring(pos1 + 1) : null);
					p.OnPrivateMessage(sender, message);
					return true;
				}
			}

			return false;
		}

		private bool ParseNoticeMsg(string msg, string[] words)
		{
			if (words.Length <= 3)
			{
				return false;
			}

			if ((words[2][0] == '#') || (words[2][0] == '&'))
			{
				Channel chan = this.FindChannel(words[2], true);

				if (null != chan)
				{
					int pos1 = msg.IndexOf(':', 1);
					chan.OnNoticeAdded(words[0], (pos1 >= 0) ? msg.Substring(pos1 + 1) : null);
					return true;
				}
			}
			else
			{
				if (true == this.createPrivatesForNotice)
				{
					Private p = FindPrivate(words[0], true);

					if (null != p)
					{
						int pos1 = msg.IndexOf(':', 1);
						p.OnPrivateMessage(words[0], (pos1 >= 0) ? msg.Substring(pos1 + 1) : null);
						return true;
					}
				}
				else
				{
					int pos1 = msg.IndexOf(':', 1);

					OnNotice(new IrcUser(this, words[0]), (pos1 >= 0) ? msg.Substring(pos1 + 1) : null);

					return true;
				}
			}

			return false;
		}

		private bool ParseNamesReply(string msg, string[] words)
		{
			if (words.Length <= 5)
			{
				return false;
			}

			string chanName = words[4];

			if ( (chanName[0] == '#') || (chanName[0] == '&') )
			{
				Channel chan = FindChannel(chanName, true);

				if (null != chan)
				{
					int indx = msg.IndexOf(':', 1);

					if (indx >= 0)
					{
						string[] nicks = msg.Substring(indx).Split(new char[2] { ' ', ':' } );

						foreach (string nick in nicks)
						{
							chan.AddTempNick(nick);
						}
					}
				}

				return true;
			}

			return false;
		}

		private bool ParseEndOfNamesReply(string msg, string[] words)
		{
			if (words.Length <= 4)
			{
				return false;
			}
				
			string chanName = words[3];

			if ( (chanName[0] == '#') || (chanName[0] == '&') )
			{
				Channel chan = FindChannel(chanName, true);

				if (null != chan)
				{
					chan.SwapNicks();
				}
			}

			return true;
		}

		private bool ParseReplyTopic(string msg, string[] words)
		{
			if (words.Length <= 4)
			{
				return false;
			}
				
			string chanName = words[3];

			if ( (chanName[0] == '#') || (chanName[0] == '&') )
			{
				Channel chan = FindChannel(chanName, true);
					
				if (null != chan)
				{
					chan.OnTopicRecieved(msg);
					return true;
				}
			} 

			return false;
		}

		private bool ParseReplyTopicSetBy(string msg, string[] words)
		{
			if (words.Length == 6)
			{
				string chanName = words[3];

				if (chanName[0] == ':') chanName = words[2].Substring(1);

				if ( (chanName[0] == '#') || (chanName[0] == '&') )
				{
					Channel chan = this.FindChannel(chanName, true);
					
					if (null != chan)
					{
						// TBD
						//chan.Topic.
						//chan.Topic.Author = 
						//chan.Topic.SetBy(words[4], Convert.ToInt32(words[5], 10));
						return true;
					}
				} 
			}
			return false;
		}

		private bool ParseJoin(string msg, string[] words)
		{
			if (words.Length == 3)
			{
				string chanName = words[2];
				if (chanName[0] == ':') chanName = chanName.Substring(1);

				Channel chan = this.FindChannel(chanName, true);

				if (null != chan)
				{
					chan.OnUserJoined(words[0]);

					return true;
				}
			}

			return false;
		}

		private bool ParsePart(string msg, string[] words)
		{
			if (words.Length > 2)
			{
				string chanName = words[2];
				if (chanName[0] == ':') chanName = chanName.Substring(1);

				Channel chan = FindChannel(chanName, true);

				if (null != chan)
				{
					int pos = msg.IndexOf(':', 1);

					chan.OnUserParted(words[0], (pos >= 0) ? msg.Substring(pos + 1) : null);

					return true;
				}
			}
			return false;
		}

		private bool ParseMode(string msg, string[] words)
		{
			if (words.Length > 2)
			{
				string chanName = words[2];
				if (chanName[0] == ':') chanName = chanName.Substring(1);

				if (false == IsChannel(chanName))
				{
					return false;
				}

				Channel chan = this.FindChannel(chanName, true);

				if (null != chan)
				{
					chan.OnModeChanged(words[0], words);

					return true;
				}
			}

			return false;
		}

		private bool ParseBanListElementRecieved(string msg, string[] words)
		{
			if (words.Length > 5)
			{
				string chanName = words[3];
				if (chanName[0] == ':') chanName = chanName.Substring(1);
				Channel chan = this.FindChannel(chanName, true);

				if (null != chan)
				{
					chan.AddBan(words[4]);
					return true;
				}
			}
			return false;
		}

		private bool ParseModeListRecieved(string msg, string[] words)
		{
			if (words.Length >= 5)
			{
				string chanName = words[3];
				if (chanName[0] == ':') chanName = chanName.Substring(1);

				Channel chan = this.FindChannel(chanName, true);

				if (null != chan)
				{
					chan.OnModesListRecieved(words);

					return true;
				}
			}
			return false;
		}

		private bool ParseBanListRecieved(string msg, string[] words)
		{
			if (words.Length >= 4)
			{
				string chanName = words[3];
				if (chanName[0] == ':') chanName = chanName.Substring(1);

				Channel chan = this.FindChannel(chanName, true);

				if (null != chan)
				{
					chan.OnBansListRecieved();
					return true;
				}
			}
			return false;
		}

		private bool ParseKick(string msg, string[] words)
		{
			if (words.Length > 3)
			{
				string chanName = words[2];
				if (chanName[0] == ':') chanName = chanName.Substring(1);

				Channel chan = this.FindChannel(chanName, true);

				if (null != chan)
				{
					int pos = msg.IndexOf(':', 1);
					
					chan.OnUserKicked(words[0], words[3], (pos >= 0) ? msg.Substring(pos + 1) : null);

					return true;
				}
			}
			return false;
		}

		private bool ParseQuit(string msg, string[] words)
		{
			if (words.Length > 1)
			{
				string quitMessage = null;

				int pos = msg.IndexOf(':', 1);
				if (pos >= 0)
				{
					quitMessage = msg.Substring(pos + 1);
				}

				foreach (Channel chan in this.channels)
				{
					chan.OnUserQuited(words[0], quitMessage);
				}

				return true;
			}
			return false;
		}

		private bool ParseNickChanged(string msg, string[] words)
		{
			if (words.Length > 2)
			{
				IrcUser user = FindUser(words[0], true);
				string newNick = (words[2][0] == ':') ? words[2].Substring(1) : words[2];
				newNick = newNick.Trim();

				if (user.Nick.ToUpper().Equals(this.user.Nick.ToUpper()))
				{
					this.user.Nick = newNick;
				}

				foreach (Channel chan in this.channels)
				{
					chan.OnNickChanged(words[0], newNick);
				}

				string upNick = user.Nick.ToUpper();

				foreach (Private p in this.privates)
				{
					if (true == p.User.Nick.ToUpper().Equals(upNick))
					{
						((IrcUser)p.User).OnNickChanged(newNick);
						break;
					}
				}

				return true;
			}

			return false;
		}

		private bool ParseTopicChanged(string msg, string[] words)
		{
			if (words.Length > 2)
			{
				string chanName = words[2];

				if (chanName[0] == ':')
				{
					chanName = chanName.Substring(1);
				}

				Channel chan = FindChannel(chanName, true);

				if (null != chan)
				{
					int pos = msg.IndexOf(':', 1);
					
					chan.OnTopicChanged(words[0], (pos >= 0) ? msg.Substring(pos + 1) : null);

					return true;
				}
			}
			return false;
		}

		private bool ParseMessage(string msg, string[] words)
		{
			if (null == msg)
			{
				return false;
			}

			if (string.Empty.Equals(msg))
			{
				return false;
			}

			if ( (msg[0] == ':') && (words.Length > 2) )
			{
				string cmd = words[1].ToUpper();

				if ("PRIVMSG".Equals(cmd))
				{
					return ParsePrivMsg(msg, words);
				} 
				else if ("NOTICE".Equals(cmd))
				{
					return ParseNoticeMsg(msg, words);
				} 
				else if ("353".Equals(cmd))
				{
					return ParseNamesReply(msg, words);
				} 
				else if ("366".Equals(cmd))
				{
					return ParseEndOfNamesReply(msg, words);
				} 
				else if ("332".Equals(cmd))
				{
					return ParseReplyTopic(msg, words);
				} 
				else if ("333".Equals(cmd))
				{
					return ParseReplyTopicSetBy(msg, words);
				} 
				else if ("JOIN".Equals(cmd))
				{
					return ParseJoin(msg, words);
				} 
				else if ("PART".Equals(cmd))
				{
					return ParsePart(msg, words);
				} 
				else if ("MODE".Equals(cmd))
				{
					return ParseMode(msg, words);
				} 
				else if ("KICK".Equals(cmd))
				{
					return ParseKick(msg, words);
				} 
				else if ("QUIT".Equals(cmd))
				{
					return ParseQuit(msg, words);
				} 
				else if ("NICK".Equals(cmd))
				{
					return ParseNickChanged(msg, words);
				} 
				else if ("TOPIC".Equals(cmd))
				{
					return ParseTopicChanged(msg, words);
				} 
				else if ("367".Equals(cmd))
				{
					return ParseBanListElementRecieved(msg, words);
				} 
				else if ("368".Equals(cmd))
				{
					return ParseBanListRecieved(msg, words);
				} 
				else if ("324".Equals(cmd))
				{
					return ParseModeListRecieved(msg, words);
				} 
				else
				{
					OnUnparsedLeftText(msg);
					return true;
				  }
			}

			return false;
		}

		internal Channel FindChannel(string channelName, bool createNew)
		{
			string name1 = channelName.Trim();

			string name = (name1[0] == ':') ? name1.Substring(1) : name1;

			if ((name[0] != '#') && (name[0] != '&'))
			{
				name = "#" + name;
			}

			string nameUpr = name.ToUpper();

			foreach (Channel chan in this.channels)
			{
				if (true == nameUpr.Equals(chan.Name.ToUpper()))
				{
					return chan;
				}
			}

			if (true == createNew)
			{
				System.Diagnostics.Debug.WriteLine("Creating channel " + name);
				Channel chan = new Channel(name, this);
				this.channels.Add(chan);
				OnChannelJoined(chan);
				return chan;
			}

			return null;
		}

		internal Private FindPrivate(string userId, bool createNew)
		{
            userId = userId.ToUpper();

			foreach (Private p in this.privates)
			{
				if (true == userId.Equals(p.User.FullId.ToUpper()))
				{
					return p;
				}
			}

			string[] words = userId.Split(new char[3] { ':', '!', '@' });

			if (words.Length > 1)
			{
                words[1] = words[1].ToUpper();

				foreach (Private p in this.privates)
				{
					{
						if (true == words[1].Equals(p.User.Nick.ToUpper()))
						{
							return p;
						}
					}
				}
			}
			
			if (true == createNew)
			{
				IrcUser usr = FindUser(userId, true);

				if (null == usr)
				{
					return null;
				}

				Private p = new Private(this, usr);
				this.privates.Add(p);
				OnPrivateChatCreated(p);
				return p;
			}
			else
			{
				return null;
			}
		}

		internal IrcUser FindUser(string fullId, bool createNew)
		{
            fullId = fullId.ToUpper();

			foreach (IrcUser user in this.users)
			{
				if (true == fullId.Equals(user.FullId.ToUpper()))
				{
					return user;
				}
			}

			if (true == createNew)
			{
				IrcUser user = new IrcUser(this, fullId);

				//if ( ("".Equals(user.Ident)) || ("".Equals(user.HostName)) )
				//{
				//    return null;
				//}

				this.users.Add(user);
				return user;
			}
			else
			{
				return null;
			}
		}

		protected virtual void OnRawMessageRecieved(string message)
		{
			if (null != this.RawMessageRecieved)
			{
				this.RawMessageRecieved(this, message);
			}
		}

		protected virtual void OnPingPong()
		{
			if (null != this.PingPong)
			{
				this.PingPong(this);
			}
		}

		protected virtual void OnConnected()
		{
			if (null != this.Connected)
			{
				this.Connected(this);
			}
		}

		internal override void OnDisconnected()
		{
			base.OnDisconnected();

			try
			{
				foreach (Channel chan in this.channels)
				{
					chan.OnDisconnected();
				}

				foreach (Private priv in this.privates)
				{
					priv.OnDisconnected();
				}
			}
			catch (Exception ex)
			{
				PrintException(ex);
			}

			this.channels.Clear();
			this.privates.Clear();

			if (null != this.Disconnected)
			{
				this.Disconnected(this);
			}
		}

		protected virtual void OnUnparsedLeftText(string text)
		{
			if (null != this.UnparsedLeftText)
			{
				this.UnparsedLeftText(this, text);
			}
		}

		protected virtual void OnChannelJoined(IChannel chan)
		{
			if ( (null != this.ChannelJoined) && (null != chan) )
			{
				this.ChannelJoined(this, chan);
			}
		}

		protected virtual void OnPrivateChatCreated(IPrivate privateChat)
		{
			if ( (null != this.PrivateChatCreated) && (null != privateChat) )
			{
				this.PrivateChatCreated(this, privateChat);
			}
		}

		protected virtual void OnNotice(IUser user, string message)
		{
			if (null != this.Notice)
			{
				this.Notice(this, user, message);
			}
		}

		private void PrintException(Exception ex)
		{
			DebugUtil.PrintException(ex);
		}

		private bool IsChannel(string name)
		{
			if ((name[0] == '#') || (name[0] == '&'))
			{
				return true;
			}
			else
			{
				return false;
			}
		}

		public bool CreatePrivatesForNotice
		{
			get
			{
				return this.createPrivatesForNotice;
			}
			set
			{
				this.createPrivatesForNotice = value;
			}
		}
	}
}
